// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.TeamFoundation.Migration.Toolkit;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.IO;
using System.Net;
using System.Diagnostics;
using Microsoft.Teamfoundation.Migration.VersionControl.SharePoint.Dws;
using System.Web;
using System.Globalization;
using System.Data.SqlClient;
using System.Data;
using System.Transactions;
using System.Xml.Serialization;
using Microsoft.TeamFoundation.Migration.Toolkit.VC;

namespace Microsoft.TeamFoundation.Migration.VersionControl.Wss
{
    public class Tfs2WssMigrationEngine : VCMigrationEngine
    {
        // We use this list to keep track of Dirs that have been renamed in the same changeset. Later rename of child item with same rename change will be skipped
        // as the rename has already been processed.
        List<RenameFile> m_renamedDirList = new List<RenameFile>();
        int currentChangeset;

        public Tfs2WssMigrationEngine(VersionControlSession session, ChangeGroupingMananger manager)
            : base(session, manager)
        {
            manager.LastHighWaterMark = new HighWaterMark<int>(session, "TFSMigrationHWM");
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        protected override void ProcessChangeGroup(ChangeGrouping group)
        {
            if (int.Parse(group.Name, CultureInfo.InvariantCulture) > currentChangeset)
            {
                // For TFS2WSS migration, group name is the changeset id in TFS. 
                currentChangeset = int.Parse(group.Name, CultureInfo.InvariantCulture);
                m_renamedDirList.Clear();
            }
            group.Status = ChangeStatus.InProgress;
            group.Save();

            ChangeStatus endStatus = ChangeStatus.Complete;
            List<string> deleteList = new List<string>();
            List<string> addUndeleteList = new List<string>();
            List<string> otherList = new List<string>();
            List<RenameFile> renameList = new List<RenameFile>();

            foreach (IMigrationAction action in group.Actions)
            {
                if (action.State == ActionState.Pending)
                {
                    try
                    {
                        switch (action.Action)
                        {
                            case ChangeAction.Add:
                                uploadTfsItemToWss(action);
                                addUndeleteList.Add(stripHost(((WssMigrationItem)action.TargetTargetItem).CanonicalUrl));
                                break;
                            case ChangeAction.Branch:
                                uploadTfsItemToWss(action);
                                otherList.Add(stripHost(((WssMigrationItem)action.TargetTargetItem).CanonicalUrl));
                                break;
                            case ChangeAction.Delete:
                                deleteWssItem(action);
                                deleteList.Add(stripHost(((WssMigrationItem)action.TargetTargetItem).CanonicalUrl));
                                break;
                            case ChangeAction.Edit:
                                uploadTfsItemToWss(action);
                                otherList.Add(stripHost(((WssMigrationItem)action.TargetTargetItem).CanonicalUrl));
                                break;
                            case ChangeAction.Encoding:
                                // skip
                                break;
                            case ChangeAction.Merge:
                                uploadTfsItemToWss(action);
                                otherList.Add(stripHost(((WssMigrationItem)action.TargetTargetItem).CanonicalUrl));
                                break;
                            case ChangeAction.Rename:
                                string oldPath = ((WssMigrationItem)action.TargetSourceItem).CanonicalUrl;
                                string newPath = ((WssMigrationItem)action.TargetTargetItem).CanonicalUrl;
                                bool parentDirAlreadyRenamed = false;
                                foreach (RenameFile renamedDir in m_renamedDirList)
                                {
                                    if ( oldPath.StartsWith(renamedDir.OldFilePath, StringComparison.InvariantCultureIgnoreCase)
                                       && string.Compare(renamedDir.NewFilePath + oldPath.Substring(renamedDir.OldFilePath.Length), newPath, StringComparison.InvariantCultureIgnoreCase) == 0)
                                    {
                                        parentDirAlreadyRenamed = true;
                                        break;
                                    }
                                }
                                if (!parentDirAlreadyRenamed)
                                {
                                    if (action.ItemType != ItemType.File)
                                    {
                                        m_renamedDirList.Add(new RenameFile(oldPath, newPath));
                                    }
                                    moveWssItem(action);
                                }
                                renameList.Add(new RenameFile(stripHost(oldPath), stripHost(newPath)));
                                break;
                            case ChangeAction.Undelete:
                                uploadTfsItemToWss(action);
                                addUndeleteList.Add(stripHost(((WssMigrationItem)action.TargetTargetItem).CanonicalUrl));
                                break;
                            case ChangeAction.Label:
                                // skip
                                break;
                            default:
                                throw new VersionControlException(string.Format(
                                    WSS2TFSResources.Culture,
                                    WSS2TFSResources.UnexpectedActionType, 
                                    action));
                        }

                        action.State = ActionState.Complete;

                        if(action.TargetTargetItem != null)
                        {
                            WssMigrationItem targetItem = (WssMigrationItem)action.TargetTargetItem;
                            UpdateConversionHistory(
                                group.SessionId,
                                string.Format(CultureInfo.InvariantCulture, "{0}#{1}#{2}", targetItem.FileId, 0, targetItem.CanonicalUrl),
                                int.Parse(group.Name, CultureInfo.InvariantCulture),
                                DateTime.UtcNow,
                                group.Comment,
                                MigrationDirection.TfsToSource);
                        }
                    }

                    catch (VCInvalidPathException ive)
                    {
                        // when this happens the path is invalid and there is no way to migrate to it.
                        // ideally we would have caught this during analysis
                        TraceManager.TraceWarning(ive.Message);
                        endStatus = ChangeStatus.Skipped;
                        action.State = ActionState.Skipped;
                    }
                    catch (FPRpcException fpe)
                    {
                        if (fpe.StatusResult != null)
                        {
                            switch (fpe.StatusResult.Status)
                            {
                                case WSSErrorCodes.ExtensionHasDoubleDots:
                                case WSSErrorCodes.FileBlockedByAdmin:  
                                case WSSErrorCodes.ThicketFileCopy:  
                                    Trace.TraceWarning(fpe.StatusResult.Message);
                                    endStatus = ChangeStatus.Skipped;
                                    action.State = ActionState.Skipped;
                                    break;
                                case WSSErrorCodes.FileLockedOrCheckedOut:
                                    Trace.TraceWarning(fpe.StatusResult.Message);
                                    // This conflict can be resolved externally. Set the change group status to 'InProgress' so that 
                                    // it will be picked up by next round of synchronization.
                                    endStatus = ChangeStatus.InProgress;
                                    action.State = ActionState.Pending;
                                    break;
                                default:
                                    Trace.TraceWarning("FPRPC Status ({0}): {1}, {2}, {3}",
                                        fpe.StatusResult.Method,
                                        fpe.StatusResult.Status,
                                        fpe.StatusResult.OSStatus,
                                        fpe.StatusResult.OSMessage);
                                    throw;
                            }
                        }
                        else
                        {                                
                            throw;
                        }
                    }
                }
            }

            // Update the master table with the latest delete/rename information
            if ((deleteList.Count > 0) || (renameList.Count > 0) || (otherList.Count > 0) || (addUndeleteList.Count > 0) )
            {
                WSSListEnumerator le = new WSSListEnumerator(((WssProvider)Session.Source).DocumentLibrary);

                le.UpdateWssMasterTable((Session)Session, deleteList, renameList, addUndeleteList, otherList);
            }

            group.Status = endStatus;
            group.Save();

            ((HighWaterMark<int>)Manager.LastHighWaterMark).Update(int.Parse(group.Name, CultureInfo.InvariantCulture));
                
        }

        private static void deleteWssItem(IMigrationAction action)
        {
            WssMigrationItem deleteTarget = (WssMigrationItem)action.TargetTargetItem;
            WssFileManager man = new WssFileManager();

            man.DeleteDocument(deleteTarget.CanonicalUrl);
        }

        private static void moveWssItem(IMigrationAction action)
        {
            TfsMigrationItem sourceItem = (TfsMigrationItem)action.SourceItem;

            if (sourceItem.DeletionId == 0)
            {
                WssMigrationItem renameSource = (WssMigrationItem)action.TargetSourceItem;
                WssMigrationItem renameTarget = (WssMigrationItem)action.TargetTargetItem;

                WssFileManager man = new WssFileManager();
                man.RenameFile(renameSource.CanonicalUrl, renameTarget.CanonicalUrl);
            }
            else
            {
                TraceManager.TraceWarning("A deleted item was carried over from a directory rename");
            }
        }

        private void uploadTfsItemToWss(IMigrationAction action)
        {
            TfsMigrationItem sourceItem = (TfsMigrationItem)action.SourceItem;
            WssMigrationItem targetItem = (WssMigrationItem)action.TargetTargetItem;

            WssFileManager man = new WssFileManager();

            if (!sourceItem.IsDirectory)
            {
                if ((action.Action == ChangeAction.Add) && (WssFileManager.DoesPathExist(targetItem.CanonicalUrl)))
                {
                    Session.OnMigrationWarning(new VersionControlEventArgs(
                        string.Format(WSS2TFSResources.Culture, WSS2TFSResources.ItemAlreadyExists, targetItem.CanonicalUrl),SystemType.Tfs));
                    return;
                }


                string tempFile = Path.GetTempFileName();
                try
                {
                    sourceItem.Download(tempFile);

                    /* to alter the metainfo to include the date-time it needs to be formated like so:
                       Example: "23 Sep 1998 09:54:51 -0700"
                       Format:  "d MMM yyyy HH:mm:ss zz"
                     */

                    Trace.TraceInformation("Uploading document to Wss: {0}", targetItem.CanonicalUrl);

                    PutDocumentResult pdr = man.PutDocument(targetItem.CanonicalUrl, tempFile, null, action.ChangeGroup.Comment);
                    if (pdr.IsStatusPacket)
                    {
                        throw new FPRpcException(
                            string.Format(WSS2TFSResources.Culture,
                                WSS2TFSResources.ErrorUploadingToWss,
                                targetItem.CanonicalUrl,
                                pdr.StatusResult.Message),
                            pdr.StatusResult);
                    }
                }
                finally
                {
                    if (File.Exists(tempFile))
                    {
                        File.Delete(tempFile);
                    }
                }
            }
            else
            {
                man.EnsureParentToPathExists(targetItem.CanonicalUrl);
                man.CreatePath(targetItem.CanonicalUrl);
            }
        }

        private static string stripHost(string CanonicalUrl)
        {
            Uri u = new Uri(CanonicalUrl);
            return u.GetComponents(UriComponents.Path, UriFormat.Unescaped);
        }


        public override MigrationDirection Direction
        {
            get 
            {
                return MigrationDirection.TfsToSource;
            }
        }
    }

    /// <summary>
    /// Known error codes from the WSS server.
    /// </summary>
    static class WSSErrorCodes
    {
        /// <summary>
        /// "You cannot copy or move a thicket file"
        /// </summary>
        public const int ThicketFileCopy = 589929;

        /// <summary>
         /// the file was blocked by the administrator
        /// </summary>
        public const int FileBlockedByAdmin = 589924;

        /// <summary>
         /// the file contains two dots in the extension (i.e. filename..ext)
        /// </summary>
        public const int ExtensionHasDoubleDots = 589936;

        /// <summary>
        /// //0x0009000E (589838) corresponds to [[V_DOC_CHECKED_OUT]]=The file "%1!.200s!" is checked out or locked for editing by %2!.40s!.
        /// </summary>
        public const int FileLockedOrCheckedOut = 589838;
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
    public struct RenameFile
    {
        public RenameFile(string oldFilePath, string newFilePath)
        {
            m_oldFilePath = oldFilePath;
            m_newFilePath = newFilePath;
            if (string.IsNullOrEmpty(newFilePath))
            {
                m_newFileName = null;
            }
            else
            {
                m_newFileName = newFilePath.Substring(newFilePath.LastIndexOf('/') + 1);
            }
        }
        public string OldFilePath
        {
            get
            {
                return m_oldFilePath;
            }
        }
        public string NewFilePath
        {
            get
            {
                return m_newFilePath;
            }
        }
        public string NewFileName
        {
            get
            {
                return m_newFileName;
            }
        }
        private string m_oldFilePath;
        private string m_newFilePath;
        private string m_newFileName;
    }
}
